import UserDao from "../dao/user-dao";
import InsUtils from "../utils/ins-utils";
import RoomUtils from "../utils/room-utils";
import BattleUtils from "../utils/battle-utils";
import BoardUtils from "../utils/board-utils";
import DateUtils from "../utils/date-utils";
import EmailUtils from "../utils/email-utils";
import {
    APP,
    BATTLE,
    BATTLE_FLOW_TYPE,
    BOOLEAN,
    CODE_TYPE,
    GAME_OVER_TYPE,
    INS_KEY,
    LOGIN_TYPE,
    PAGE_STATUS,
    ROOM_FLOW_TYPE,
    ROOM_JOIN_TYPE,
    ROOM_STATUS,
    USER_ROLE,
    USER_STATUS,
    USER_TYPE
} from "../configs/config";
import SocketUtils from "../utils/socket-utils";
import CryptorUtils from "../utils/cryptor-utils";
import ValidCodeDao from "../dao/valid-code-dao";
import BattleChatDao from "../dao/battle-chat-dao";
import BattleDao from "../dao/battle-dao";
import BattleHistoryDao from "../dao/battle-history-dao";
import BattleFlowDao from "../dao/battle-flow-dao";
import UserStateDao from "../dao/user-state-dao";
import BattleUserDao from "../dao/battle-user-dao";
import VersionDao from "../dao/version-dao";
import FileUtils from "../utils/file-utils";
import ConstUtils from "../utils/const-utils";
import {Transaction} from "../aop/transaction-aop";
import {CatchException} from "../aop/exception-aop";
import ShareDao from "../dao/share-dao";
import RoomFlowDao from "../dao/room-flow-dao";
import CheckWin from "../utils/check-win";

const md5 = require("md5");
const AsyncLock = require('async-lock');

const lock = new AsyncLock();
const insUtils = new InsUtils();
const roomUtils = new RoomUtils();
const battleUtils = new BattleUtils();
const boardUtils = new BoardUtils();
const dateUtils = new DateUtils();
const emailUtils = new EmailUtils();
const socketUtils = new SocketUtils();
const fileUtils = new FileUtils();
const constUtils = new ConstUtils();
const checkWin = new CheckWin();

class SocketServiceImpl {
    /**
     * 版本检测
     * @returns {Promise<void>}
     */
    getVersionImpl = async (socket: any) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'getVersionImpl');
        const version = await VersionDao.getClientVersion();
        log.info(`当前版本为：${version}`);
        socket.emit('versionRespApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '客户端版本',
            data: {
                version: version,
            },
        }));
    }

    /**
     * 获取版本详情
     * @param socket
     * @param fn
     */
    getVersionDetailImpl = async (socket: any, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'getVersionDetailImpl');

        const vDetailList = await VersionDao.getVersionDetailList();
        log.info(`版本信息获取完成`);
        fn(CryptorUtils.encrypt({
            code: 'success',
            msg: '游戏版本信息',
            data: vDetailList,
        }));
    }

    /**
     * 登录核心逻辑处理
     * @param socket
     * @param userId
     * @param password
     * @param ticket
     * @param type
     * @param fn
     */
    @Transaction
    @CatchException
    async loginImpl(socket: any, userId: string, password: string, ticket: string, type: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'loginImpl', {userId});
        const token = socketUtils.getToken(socket);
        const ip = socketUtils.getClientIp(socket);
        const finger = socketUtils.getFinger(socket);
        log.info(`当前登录的token：[${token}]，指纹：[${finger}]`);

        const where = [];
        if (type === LOGIN_TYPE.USER_PASS) {
            // 兼容使用邮箱登录
            if (/^[A-Za-z0-9]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(userId)) {
                where.push(['email', userId]);
            } else {
                where.push(['user_id', userId]);
            }
            where.push(['password', password]);
        } else if (type === LOGIN_TYPE.TICKET) {
            where.push(['user_id', userId]);
            where.push(['ticket', ticket]);
            // 凭证登录不安全，加上以前登录过的指纹进行验证
            // where.push(['finger', finger])
            // where.push(['ip', ip]);
        }
        // 调用数据库进行登录
        const userRows: any = await UserDao.queryUserTable(where);
        log.info(`登录数据为：`, userRows);
        // 校验数据
        if (userRows.length === 1) {
            // 优先检查账号有没有被禁用
            const [user] = userRows;
            const {dataStatus} = user;
            if (!dataStatus || dataStatus === BOOLEAN.SHORT_NO) {
                fn(CryptorUtils.encrypt({
                    code: 'fail',
                    msg: '账号已被禁用',
                }));
                return {status: false, kickToken: null};
            }
            const updUserColumns = [];
            updUserColumns.push(['ip', ip]);
            updUserColumns.push(['finger', finger])

            // 更新用户端数据
            await UserDao.updateUserTable(updUserColumns, [['user_id', user.userId]]);

            // 判断是否已有用户在登录了
            const userState = await UserStateDao.getUserStateByUserId(user.userId);
            log.info(`查询账号是否冲突时，检测到[${user.userId}]游离数据为:`, userState);

            if (!userState) {
                log.info(`查询账号是否冲突时，[${user.userId}]无游离数据，准备创建`);
                // 记录初始数据
                await UserStateDao.createUserState(user.userId, token);
                log.info(`已创建初始状态数据`);
            } else {
                // 清除断线时间
                await UserStateDao.updateUserStateTable([['disconnect_time', null], ['token', token]], [['user_id', user.userId]]);
            }

            // 通知客户端（回调客户端提供的function）
            fn(CryptorUtils.encrypt({
                code: 'success',
                msg: '登录成功(账号密码)',
                data: {ticket: user.ticket, userId: user.userId}
            }));
            log.info(`[${user.userId}]已登录`);
            return {status: true, kickToken: userState?.token};
        } else {
            const msg = (type === LOGIN_TYPE.USER_PASS ? '账号或密码错误' : '登录凭证已过期');
            fn(CryptorUtils.encrypt({code: 'fail', msg: msg}));
            log.info(msg);
            return {status: false, kickToken: null};
        }
    }

    /**
     * 获取用户详情
     * @param socket
     * @param userId
     * @param fn
     * @return {Promise<void>}
     */
    userDetailImpl = async (socket: any, userId: string, fn: Function) => {
        const user: any = await UserDao.getUserByPrimaryKey(userId);
        if (!user) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '查询不到用户信息'}));
        } else {
            fn(CryptorUtils.encrypt({
                code: 'success',
                msg: '查询用户信息成功',
                data: {
                    user: {
                        pkFailCount: user.pkFailCount,
                        pkOfflineCount: user.pkOfflineCount,
                        pkPeaceCount: user.pkPeaceCount,
                        pkTotalCount: user.pkTotalCount,
                        pkWinCount: user.pkWinCount,
                        score: user.score,
                        userId: user.userId,
                        userType: user.userType,
                        userName: user.userName,
                        iconUrl: fileUtils.getShowPath(user.iconUrl),
                        scoreSort: await UserDao.getScoreSortByUserId(userId)
                    }
                }
            }));
        }
    }

    /**
     * 修改用户信息
     * @param socket
     * @param userId
     * @param modifyDetail
     * @param fn
     */
    @Transaction
    async modifyUserDetailImpl(socket: any, userId: string, modifyDetail: any, fn: Function) {
        const user: any = await UserDao.getUserByPrimaryKey(userId);
        if (!user) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '查询不到用户信息'}));
        } else if (USER_TYPE.TOURIST_USER === user.userType) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '游客无法修改信息'}));
        } else {
            const {userName, iconUrl} = modifyDetail;
            const updColumn = [];
            userName && updColumn.push(['user_name', userName]);
            iconUrl && updColumn.push(['icon_url', iconUrl]);
            if (updColumn.length > 0) {
                await UserDao.updateUserTable(updColumn, [['user_id', userId]]);
            }
            fn(CryptorUtils.encrypt({
                code: 'success',
                msg: '修改用户信息成功',
            }));
        }
    }

    /**
     * 自动生成并注册游客账号
     * @param socket
     * @param token
     * @param finger
     * @param fn
     */
    @Transaction
    @CatchException
    async autoGenerateTouristImpl(socket: any, token: string, finger: string, fn: Function) {
        const maxCount = 10;
        let count = 0;
        // 账户密码
        const password = md5(constUtils.getRandomId(8));
        // 登录凭证
        const ticket = socketUtils.generateTicket();
        let userName = `棋手 ${constUtils.getRandomId(6, 10)}`;
        do {
            const userId = `tour_${constUtils.getRandomId(8, 10)}`;
            // 检测数据库中是否有相同的账号
            const user: any = await UserDao.getUserByPrimaryKey(userId);
            if (!user) {
                // 未找到相同账号，直接注册
                const ip = socketUtils.getClientIp(socket);
                await UserDao.createUser(userId, password, userName, null, USER_TYPE.TOURIST_USER, ticket, finger, ip);
                // 返回注册信息
                fn(CryptorUtils.encrypt({
                    code: 'success',
                    msg: '您的游客凭证信息',
                    data: {
                        ticket: ticket,
                        userId: userId,
                    }
                }));
                socket.captcha = null;
                break;
            }
        } while ((++count) < maxCount);
        // 超过指定次数
        if (count >= maxCount) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '账号生成失败，请重试'}));
        }
    }


    /**
     * 注册核心逻辑处理
     * @param socket
     * @param userId
     * @param userName
     * @param password
     * @param email
     * @param validCode
     * @param fn
     */
    @Transaction
    @CatchException
    async registerImpl(socket: any, userId: string, userName: string, password: string, email: string, validCode: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'registerImpl', {userId});

        // 查询用户是否存在
        const user: any = await UserDao.getUserByPrimaryKey(userId);
        if (user) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '账号已被使用'}));
            log.info(`账号[${userId}]已被使用`);
        } else if (email && !(await ValidCodeDao.validateEmailAndCode(userId, email, validCode, APP.CODE_VALID_TIME_MINUTES))) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '验证码错误或已过期'}));
            log.info(`账号[${userId}]使用的验证码[${validCode}]已过期`);
        } else {
            socket.captcha = null;
            // 将验证码失效
            await ValidCodeDao.updateValidCodeStatus(userId, email, validCode, BOOLEAN.SHORT_NO);
            // 创建用户数据
            const ticket = socketUtils.generateTicket();
            const finger = socketUtils.getFinger(socket);
            const ip = socketUtils.getClientIp(socket);
            const isSuc = await UserDao.createUser(userId, password, userName, email, USER_TYPE.REGISTER_USER, ticket, finger, ip);
            fn(CryptorUtils.encrypt({code: isSuc ? 'success' : 'fail', msg: isSuc ? '注册成功' : '注册失败'}));
            log.info(`账号[${userId}]${isSuc ? '注册成功' : '注册失败'}`);
        }
    }

    /**
     * 发送验证码核心逻辑处理
     * @param socket
     * @param userId
     * @param userName
     * @param email
     * @param fn
     */
    @Transaction
    @CatchException
    async sendValidCodeImpl(socket: any, userId: string, userName: string, email: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'sendValidCodeImpl', {userId});

        const user: any = await UserDao.getUserByPrimaryKey(userId);
        if (user) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '账号已被使用'}));
            return;
        }
        const emailRows: any = await UserDao.queryUserTable([['email', email]]);
        if (emailRows.length > 0) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '邮箱已被使用'}));
        } else if (!await emailUtils.checkSendStatus(userId, email, APP.CODE_TODAY_MAX_SEND_COUNT)) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '该邮箱已超过当日限发次数'}));
        } else {
            const validCode = constUtils.getRandomId(6);
            const subject = '[中国象棋]注册验证码';
            const html = `尊敬的${userName}，本次验证码为：<span style="color: blue">${validCode}</span>，${APP.CODE_VALID_TIME_MINUTES}分钟内有效。`;
            const sendResult = await emailUtils.sendEmail(userId, subject, email, html);
            if (sendResult) {
                await ValidCodeDao.createValidCode(userId, email, validCode, CODE_TYPE.USER_REGISTER);
                fn(CryptorUtils.encrypt({code: 'success', msg: '验证码发送成功'}));
            } else {
                fn(CryptorUtils.encrypt({code: 'fail', msg: '发送失败，邮箱错误'}));
            }
        }
    }

    /**
     * 找回密码
     * @param socket
     * @param userId
     * @param email
     * @param password
     * @param validCode
     * @param fn
     */
    @Transaction
    @CatchException
    async forgetPasswordImpl(socket: any, userId: string, email: string, password: string, validCode: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'forgetPasswordImpl', {userId});
        // 查询用户是否存在
        const user: any = await UserDao.getUserByPrimaryKey(userId);
        if (!user) {
            // 防止恶意嗅探，提示非本意信息
            fn(CryptorUtils.encrypt({code: 'fail', msg: '账号或邮箱错误'}));
        } else if (email !== user.email) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '账号与邮箱不匹配'}));
        } else {
            if (!await ValidCodeDao.validateEmailAndCode(userId, user.email, validCode, APP.CODE_VALID_TIME_MINUTES)) {
                fn(CryptorUtils.encrypt({code: 'fail', msg: '验证码错误或已过期'}));
                log.info(`账号[${userId}]使用的验证码[${validCode}]已过期`);
            } else {
                // 将验证码失效
                await ValidCodeDao.updateValidCodeStatus(userId, user.email, validCode, BOOLEAN.SHORT_NO);
                // 修改密码
                await UserDao.updateUserPassword(userId, password);
                fn(CryptorUtils.encrypt({code: 'success', msg: '修改成功'}));
                log.info(`账号[${userId}]密码已修改`);
            }
        }
    }

    /**
     * 忘记密码发送邮件
     * @param socket
     * @param userId
     * @param email
     * @param fn
     */
    @Transaction
    @CatchException
    async forgetPasswordSendValidCodeImpl(socket: any, userId: string, email: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'forgetPasswordSendValidCodeImpl', {userId});

        const user: any = await UserDao.getUserByPrimaryKey(userId);
        if (!user) {
            // 账号不存在，但为了防止嗅探账号，此处提示未绑定邮箱
            fn(CryptorUtils.encrypt({code: 'fail', msg: '未绑定邮箱，请联系管理员'}));
        } else if (!user.email) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '未绑定邮箱，请联系管理员'}));
        } else if (email !== user.email) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '账号与邮箱不匹配'}));
        } else if (!(await emailUtils.checkSendStatus(userId, user.email, APP.CODE_TODAY_MAX_SEND_COUNT))) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '该邮箱已超过当日限发次数'}));
        } else {
            const validCode = constUtils.getRandomId(6);
            const subject = '[中国象棋]找回密码验证码';
            const html = `尊敬的${user.userName}，本次验证码为：<span style="color: blue">${validCode}</span>，${APP.CODE_VALID_TIME_MINUTES}分钟内有效。`;
            const sendResult = await emailUtils.sendEmail(userId, subject, user.email, html);
            if (sendResult) {
                await ValidCodeDao.createValidCode(userId, user.email, validCode, CODE_TYPE.FORGET_PASSWORD);
                fn(CryptorUtils.encrypt({code: 'success', msg: '验证码发送成功'}));
            } else {
                fn(CryptorUtils.encrypt({code: 'fail', msg: '发送失败，请重试'}));
            }
        }
    }


    /**
     * 退出核心逻辑处理
     * @param socket
     * @param userId
     * @param fn
     */
    loginOutImpl = async (socket: any, userId: string, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'loginOutImpl', {userId});

        await UserStateDao.deleteUserState(userId);
        log.info(`[${userId}]退出登录`);
    }

    /**
     * 获取观战列表核心逻辑
     * @param socket
     * @param userId
     * @param pageNum
     * @param pageSize
     * @param fn
     */
    watchListImpl = async (socket: any, userId: string, pageNum: number, pageSize: number, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'watchListImpl', {userId});

        // 分页获取观战列表
        const allWatchList = await BattleUserDao.allWatchListByPage(pageNum, pageSize);
        const {dataTotal, list} = allWatchList;
        fn(CryptorUtils.encrypt({
            code: 'success',
            msg: '获取观战列表',
            data: {
                list: list,
                pageSize: pageSize,
                pageNum: pageNum,
                dataTotal: dataTotal,
            },
        }));
        log.info(`请求了观战列表，共[${list.length}]条数据`);
    }

    /**
     * 加入观战核心逻辑
     * @param socket
     * @param userId
     * @param roomId
     * @param fn
     */
    @Transaction
    @CatchException
    async joinWatchImpl(socket: any, userId: string, roomId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'joinWatchImpl', {userId, roomId});

        // 检查是否还有正在进行的对局
        const isInBattle = await BattleDao.checkUserInBattle(userId);
        if (isInBattle) {
            log.info("加入观战时检测到该用户正在对局，开始进行对局恢复");
            // 获取该房间的基本信息
            const [battleInfo]: any = await BattleDao.getBattleInfoList(userId, ROOM_STATUS.BATTLE);
            log.info("用户对应的对战信息为:", battleInfo);
            // 设置基本信息
            const columns: any = [];
            columns.push(['user_page', PAGE_STATUS.BOARD]);
            columns.push(['room_id', battleInfo.roomId]);
            columns.push(['battle_id', battleInfo.battleId]);
            await UserStateDao.updateUserStateTable(columns, [['user_id', userId]]);

            log.info("已更新游离状态，开始正式恢复数据");
            await this.recoverDetail(socket, userId, battleInfo.roomId, battleInfo.battleId);
            return;
        }

        const battleData: any = await BattleDao.queryBattleTable([['room_id', roomId], ['room_status', ROOM_STATUS.BATTLE]]);
        log.info(`[${roomId}]号房间数据为：`, battleData);
        if (battleData.length === 0) {
            socket.emit('joinWatchRespApi', CryptorUtils.encrypt({code: 'fail', msg: '对局已经结束'}));
            // 更新当前用户的观战数据
            const columns = [];
            columns.push(['battle_id', null]);
            columns.push(['user_status', null]);
            columns.push(['user_page', PAGE_STATUS.PLATFORM]);
            columns.push(['room_id', null]);
            await UserStateDao.updateUserStateTable(columns, [['user_id', userId]]);
            log.info(`[${roomId}]号房间对局已经结束`);
        } else {
            const [battle] = battleData;
            // 在用户游离表中记录观战数据
            const columns = [];
            columns.push(['battle_id', battle.id]);
            columns.push(['user_status', USER_STATUS.WATCH]);
            columns.push(['user_page', PAGE_STATUS.WATCH]);
            columns.push(['room_id', roomId]);
            await UserStateDao.updateUserStateTable(columns, [['user_id', userId]]);
            log.info(`[${roomId}]号房间数据已变更`);

            // 获取用户数据
            const battleUserData = await battleUtils.getBattleData(userId, battle.id, insUtils.getIns(INS_KEY.JOB_KEY));
            if (battleUserData) {
                // 加入该房间（socket）
                socket.join(roomId);

                const user: any = battleUserData.find((user: any) => user.first);
                const enemy: any = battleUserData.find((user: any) => !user.first);
                socket.emit('joinWatchRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: '获取房间的观战数据成功',
                    data: {
                        roomUser: user,
                        roomEnemy: enemy,
                        battleId: battle.id
                    },
                }));
                const currUser: any = await UserDao.getUserByPrimaryKey(userId);
                // 发送加入观战通知
                socket.to(roomId).emit('watchNotifyRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: `[${userId}]加入观战`,
                    data: {
                        action: 'add',
                        // action: add 和 sub 时 watchUserId 必需有值
                        watchUserId: currUser.userId,
                        watchUserName: currUser.userName,
                        // action: update 时 watchUserList 为全量数据
                        watchUserList: [],
                    }
                }));
                log.info(`[${roomId}]号房间`);
            } else {
                socket.emit('joinWatchRespApi', CryptorUtils.encrypt({code: 'fail', msg: '无法获取到对局'}));
                log.info(`[${roomId}]号房间的观战数据无法被获取`);
            }
        }
    }

    /**
     * 离开观战核心逻辑
     * @param socket
     * @param userId
     * @param roomId
     * @param fn
     */
    @Transaction
    @CatchException
    async leaveWatchImpl(socket: any, userId: string, roomId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'leaveWatchImpl', {userId, roomId});

        log.info(`请求离开观战房间[${roomId}]`);
        if (userId && roomId) {
            // 在用户游离表中记录观战数据
            const columns = [];
            columns.push(['battle_id', null]);
            columns.push(['user_status', null]);
            columns.push(['user_page', PAGE_STATUS.PLATFORM]);
            columns.push(['room_id', null]);
            await UserStateDao.updateUserStateTable(columns, [['user_id', userId]]);
            const user: any = UserDao.getUserByPrimaryKey(userId);
            // 发送离开观战通知
            socket.to(roomId).emit('watchNotifyRespApi', CryptorUtils.encrypt({
                code: 'success',
                msg: `[${userId}]离开观战`,
                data: {
                    action: 'sub',
                    // action: add 和 sub 时 watchUserId 必需有值
                    watchUserId: userId,
                    watchUserName: user.userName,
                    // action: update 时 watchUserList 为全量数据
                    watchUserList: [],
                }
            }));
            log.info(`已离开[${roomId}]号房间的观战`);
        }
        // socket离开
        socket.leave(roomId);
        fn(CryptorUtils.encrypt({code: 'success', message: '已离开房间'}));
    }

    /**
     * 加入房间核心逻辑
     * @param socket
     * @param userId
     * @param joinType
     * @param historyRoomIds
     * @param appointRoomId
     * @param fn
     */
    @Transaction
    @CatchException
    async joinRoomImpl(socket: any, userId: string, joinType: string, historyRoomIds: string[], appointRoomId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'joinRoomImpl', {userId});

        if (appointRoomId && (Number(appointRoomId) <= 0 || Number(appointRoomId) > BATTLE.MAX_ROOM)) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '无此房间号'}));
            return;
        }
        const userState = await UserStateDao.getUserStateByUserId(userId);
        log.debug("当前用户的游离数据为：", userState);
        if (!userState) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '无法查询到用户游离状态信息'}));
            return;
        }
        // 检查是否还有正在进行的对局
        const isInBattle = await BattleDao.checkUserInBattle(userId);
        if (isInBattle) {
            log.info("加入房间时检测到该用户正在对局，开始进行对局恢复");
            // 获取该房间的基本信息
            const [battleInfo]: any = await BattleDao.getBattleInfoList(userId, ROOM_STATUS.BATTLE);
            log.info("用户对应的对战信息为:", battleInfo);
            // 设置基本信息
            const columns: any = [];
            columns.push(['user_page', PAGE_STATUS.BOARD]);
            columns.push(['room_id', battleInfo.roomId]);
            columns.push(['battle_id', battleInfo.battleId]);
            await UserStateDao.updateUserStateTable(columns, [['user_id', userId]]);

            log.info("已更新游离状态，开始正式恢复数据");
            await this.recoverDetail(socket, userId, battleInfo.roomId, battleInfo.battleId);
            return;
        }

        // 查询用户信息
        const user: any = await UserDao.getUserByPrimaryKey(userId);
        if (!user) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '无法查询用户信息'}));
            return;
        }

        // 获取系统分配的房间号
        const roomId = await roomUtils.distributionRoom(userId, user.userType, historyRoomIds, appointRoomId);
        if (!roomId) {
            // 加入房间失败
            fn(CryptorUtils.encrypt({code: 'fail', msg: '服务器爆满，请稍后再试'}));
            log.info(`服务器爆满`);
        } else {
            // 获取房间信息
            const currRoom: any = await UserStateDao.getRoomData(roomId);
            if (currRoom.length <= 2) {
                // 检查用户是否已分配过房间，若分配过就先退出
                if (userState.roomId) {
                    log.info(`用户正在房间中，房间号[${userState.roomId}]`);
                    await this.leaveRoomImpl(socket, userId, userState.roomId, (resp: any) => {
                        log.info(`用户离开房间[${userState.roomId}]的结果为：`, resp);
                    });
                }

                // 加入房间
                socket.join(roomId);
                const io = insUtils.getIns(INS_KEY.IO_KEY);
                io.emit('onlineCountRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: '在线数据累计',
                    data: {
                        action: 'add',
                        onlineCount: 1,
                        battleCount: 0,
                    },
                }))
                // 判断先后手
                if (currRoom.length === 2) {
                    // 房间有人时，我方后加入所以为后手
                    const enemy = currRoom.find((user: any) => user.userId !== userId);
                    await UserStateDao.updateUserStateTable([['first', false], ['is_room_admin', false], ['room_status', ROOM_STATUS.MULTIPLE_WAIT]], [['user_id', userId]]);
                    await UserStateDao.updateUserStateTable([['first', true], ['is_room_admin', true], ['room_status', ROOM_STATUS.MULTIPLE_WAIT]], [['user_id', enemy.userId]]);
                } else {
                    // 房间无人时，我方为先手
                    const columns = [];
                    columns.push(['first', true]);
                    columns.push(['is_room_admin', true]);
                    columns.push(['room_status', ROOM_STATUS.WAIT]);
                    await UserStateDao.updateUserStateTable(columns, [['user_id', userId]]);
                }

                // 更新我方加入房间后的相关数据
                const columns = [];
                columns.push(['is_ready', false]);
                columns.push(['user_page', joinType === ROOM_JOIN_TYPE.RANDOM ? PAGE_STATUS.PLAYER_RANDOM : PAGE_STATUS.PLAYER_FREEDOM]);
                columns.push(['join_type', joinType]);
                await UserStateDao.updateUserStateTable(columns, [['user_id', userId]]);

                log.info(`[${userId}]加入[${roomId}]号房间`);
                await this.handleRoomData(socket, userId, roomId);
                // 通知房间内的用户（敌人、观战用户）
                socket.to(roomId).emit('enemyJoinRoomRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: '对手加入房间',
                    data: {
                        user: currRoom.find((user: any) => user.userId === userId)
                    }
                }));
                // 发送全局信息
                io.emit('joinRoomRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: '全局-用户加入房间',
                    data: {
                        roomId: roomId,
                        userId: userId,
                    },
                }));
                fn(CryptorUtils.encrypt({
                    code: 'success',
                    msg: '加入房间成功',
                    data: {
                        roomId: roomId,
                    },
                }));
            } else {
                // 调整房间号
                await UserStateDao.updateUserStateTable([['room_id', userState.roomId]], [['user_id', userId]]);
                log.info(`[${userId}]无法加入[${roomId}]号房间(人数超过两人)`, currRoom);
                // 通知，加入房间失败
                fn(CryptorUtils.encrypt({code: 'fail', msg: '无法加入房间，请重试'}));
            }
        }
    }

    /**
     * 玩家进行准备
     * @param socket
     * @param userId
     * @param roomId
     * @param fn
     */
    @Transaction
    @CatchException
    async userReadyImpl(socket: any, userId: string, roomId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'userReadyImpl', {userId, roomId});

        const userState = await UserStateDao.getUserStateByUserId(userId);
        log.info(`[${userId}]所对应的用户信息为:`, userState);
        if (userState && userState.isReady) {
            log.info(`[${userId}]所在的房间:[${roomId}]已进行准备，本次请求取消`);
            return;
        }

        log.info(`我方[${userId}]开始更改房间:[${roomId}]的准备状态`);
        await UserStateDao.updateUserStateTable([['is_ready', true]], [['user_id', userId]]);
        log.info(`我方[${userId}]已准备，房间:[${roomId}]`);

        // 判断房间内是否有用户
        const room: any = await UserStateDao.getRoomData(roomId);
        log.info(`房间[${roomId}]数据为:`, room);
        const user = room.find((user: any) => user.userId === userId);
        const enemy = room.find((user: any) => user.userId !== userId);

        const io = insUtils.getIns(INS_KEY.IO_KEY);
        if (enemy) {
            // 通知对手，我方已准备(此事件客户端会主动查寻房间数据)
            socket.to(roomId).emit('enemyJoinRoomRespApi', CryptorUtils.encrypt({
                code: 'success',
                msg: '对手加入房间通知',
                data: {
                    user: user
                }
            }));
            // 判断是否可以进行对局
            if (enemy.isReady) {
                // 保存相关数据
                const playOne = {
                    ...user,
                    stepCount: 0,
                    isRedMove: true,
                    historyMoveStep: [],
                    gameMap: [],
                    stepTime: BATTLE.STEP_TIME_SECONDS,
                    allTime: BATTLE.TOTAL_TIME_SECONDS,
                    basicStepTime: BATTLE.STEP_TIME_SECONDS,
                    basicAllTime: BATTLE.TOTAL_TIME_SECONDS,
                    readSeconds: BATTLE.READ_SECONDS,
                };
                const playTwo = {
                    ...enemy,
                    stepCount: 0,
                    isRedMove: true,
                    historyMoveStep: [],
                    gameMap: [],
                    stepTime: BATTLE.STEP_TIME_SECONDS,
                    allTime: BATTLE.TOTAL_TIME_SECONDS,
                    basicStepTime: BATTLE.STEP_TIME_SECONDS,
                    basicAllTime: BATTLE.TOTAL_TIME_SECONDS,
                    readSeconds: BATTLE.READ_SECONDS,
                };
                // 保存棋盘数据，不需要区分先后方
                const saveBoardResp = await this.saveBoardData(roomId, playOne, playTwo);
                if (!saveBoardResp.result) {
                    log.info(`[${userId}]所在的房间:[${roomId}]无法创建对局(可能已创建)，本次请求取消`);
                    return;
                }
                log.info(`匹配成功，双方数据为:`, playOne, playTwo);
                io.emit('onlineCountRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: '在线数据累计',
                    data: {
                        action: 'add',
                        // 在线人数不变
                        onlineCount: 0,
                        // 新增一场对局
                        battleCount: 1,
                    },
                }));
            } else {
                log.info(`[${userId}]已准备但房间[${roomId}]中的敌对方未准备`);
            }
        } else {
            log.info(`[${userId}]已准备但房间[${roomId}]无敌对方`);
        }
        // 通知客户端，客户端会主动同步房间信息
        fn(CryptorUtils.encrypt({code: 'success', msg: '我方已准备通知'}));
        // 房间状态变更事件(桌子列表事件)
        io.emit('roomStatusChangeRespApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '全局-房间状态改变通知(新开对局)',
            data: {
                roomId: roomId,
            },
        }))
    }

    /**
     * 离开房间核心逻辑
     * @param socket
     * @param userId
     * @param roomId
     * @param fn
     */
    @Transaction
    @CatchException
    async leaveRoomImpl(socket: any, userId: string, roomId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'leaveRoomImpl', {userId, roomId});

        log.info(`[${userId}]请求离开对战房间[${roomId}]`);
        // 检查用户是否正在对局
        const isInBattle = await BattleDao.checkUserInBattle(userId);
        if (isInBattle) {
            log.info("离开房间时检测到该用户正在对局，开始进行对局恢复");
            // 获取该房间的基本信息
            const [battleInfo]: any = await BattleDao.getBattleInfoList(userId, ROOM_STATUS.BATTLE);
            log.info("用户对应的对战信息为:", battleInfo);
            // 设置基本信息
            const columns: any = [];
            columns.push(['user_page', PAGE_STATUS.BOARD]);
            columns.push(['room_id', battleInfo.roomId]);
            columns.push(['battle_id', battleInfo.battleId]);
            await UserStateDao.updateUserStateTable(columns, [['user_id', userId]]);

            log.info("已更新游离状态，开始正式恢复数据");
            await this.recoverDetail(socket, userId, battleInfo.roomId, battleInfo.battleId);
            fn(CryptorUtils.encrypt({code: 'fail', msg: '已在对局中'}));
        } else {
            // 调用离开房间的公共方法
            await roomUtils.leaveRoom(userId, roomId);
            // 离开系统为它分配的房间
            socket.leave(roomId);
            // 通知客户端逻辑处理完成
            fn(CryptorUtils.encrypt({code: 'success', msg: '已离开房间'}));
        }
    }

    /**
     * 发起求和核心逻辑
     * @param socket
     * @param userId
     * @param roomId
     * @param battleId
     * @param fn
     */
    @Transaction
    @CatchException
    async sendPeaceImpl(socket: any, userId: string, roomId: string, battleId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'sendPeaceImpl', {userId, roomId, battleId});

        // 更新求和状态
        await UserStateDao.updateBattlePeace(userId, BOOLEAN.YES, userId);

        socket.to(roomId).emit('sendPeaceRespApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '对方发起求和',
        }));

        fn(CryptorUtils.encrypt({code: 'success', msg: '求和请求发送成功'}));
        log.info(`[${userId}]在[${roomId}]号房间发起了求和`);
    }

    /**
     * 发起求和结果处理
     * @param socket
     * @param userId
     * @param roomId
     * @param result
     * @param battleId
     * @param fn
     */
    @Transaction
    @CatchException
    async sendPeaceResultImpl(socket: any, userId: string, roomId: string, result: string, battleId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'sendPeaceResultImpl', {
            userId,
            roomId,
            battleId
        });

        // 更新求和状态
        await UserStateDao.updateBattlePeace(userId, BOOLEAN.NO, null);
        // 记录求和流水
        const where = [['room_id', roomId], ['battle_id', battleId], ['user_status', USER_STATUS.BATTLE]];
        const userStateRows: any = await UserStateDao.queryUserStateTable(where);
        const enemyUser = userStateRows.find((user: any) => user.userId != userId);
        const peaceResult = BOOLEAN.AGREE === result ? "Y" : "N";
        enemyUser && await BattleFlowDao.saveBattleFlow(enemyUser.userId, roomId, battleId, BATTLE_FLOW_TYPE.PEACE, peaceResult);

        socket.to(roomId).emit('sendPeaceResultRespApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '对方处理了求和意愿',
            data: {
                result: result,
            },
        }));
        fn(CryptorUtils.encrypt({code: 'success', msg: '已处理完求和请求'}));
        log.info(`[${userId}]在[${roomId}]号房间处理了求和意愿，结果为:${result}`);

        // 同意和棋时进行游戏结算
        if (BOOLEAN.AGREE === result) {
            const userState = await UserStateDao.getUserStateByUserId(userId);
            if (!userState) {
                log.info(`用户[${userId}]无对战房间信息`);
            } else {
                await roomUtils.handleGameResult(userState.battleId, userId, GAME_OVER_TYPE.USER_PEACE);
            }
        }
    }

    /**
     * 请求悔棋
     * @param socket
     * @param userId
     * @param roomId
     * @param battleId
     * @param fn
     */
    @Transaction
    @CatchException
    async backMoveImpl(socket: any, userId: string, roomId: string, battleId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'backMoveImpl', {userId, roomId, battleId});

        // 更新悔棋状态
        await BattleDao.updateBattleBackMove(battleId, BOOLEAN.YES, userId);

        socket.to(roomId).emit('backMoveRespApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '对方请求悔棋',
            data: {
                userId: userId,
            },
        }));

        fn(CryptorUtils.encrypt({code: 'success', msg: '请求悔棋成功'}));
        log.info(`[${userId}]在[${roomId}]号房间请求悔棋`);
    }

    /**
     * 悔棋结果核心逻辑
     * @param socket
     * @param userId
     * @param roomId
     * @param result
     * @param battleId
     * @param fn
     */
    @Transaction
    @CatchException
    async backMoveResultImpl(socket: any, userId: string, roomId: string, result: string, battleId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'backMoveResultImpl', {
            userId,
            roomId,
            battleId
        });

        // 更新悔棋状态
        await BattleDao.updateBattleBackMove(battleId, BOOLEAN.NO, null);
        // 记录悔棋流水
        const userStateRows: any = await UserStateDao.queryUserStateTable([['room_id', roomId], ['battle_id', battleId], ['user_status', USER_STATUS.BATTLE]]);
        const enemyUser = userStateRows.find((user: any) => user.userId != userId);
        await BattleFlowDao.saveBattleFlow(enemyUser.userId, roomId, battleId, BATTLE_FLOW_TYPE.BACK_MOVE, BOOLEAN.AGREE === result ? "Y" : "N");

        // 拒绝情况下直接返回
        if (BOOLEAN.REJECT === result) {
            // 通知对手
            socket.to(roomId).emit('backMoveResultRespApi', CryptorUtils.encrypt({
                code: 'success',
                msg: '对方处理了悔棋意愿',
                data: {result: result, userId: userId},
            }));
            log.info(`[${userId}]在[${roomId}]号房间处理悔棋意愿：[${result}]`);
        } else if (BOOLEAN.AGREE === result) {
            const jobInstance = insUtils.getIns(INS_KEY.JOB_KEY);
            // 调用方法操作棋盘数据
            const where = [];
            where.push(['user_id', userId]);
            where.push(['room_id', roomId]);
            where.push(['room_status', ROOM_STATUS.BATTLE]);
            const userStateData: any = await UserStateDao.queryUserStateTable(where);
            if (userStateData.length === 0) {
                // 通知对手
                socket.to(roomId).emit('backMoveResultRespApi', CryptorUtils.encrypt({code: 'fail', msg: '棋盘数据错误'}));
                // 通知我方
                fn(CryptorUtils.encrypt({code: 'fail', msg: '棋盘数据错误'}));
                log.info(`[${userId}]悔棋时，无法获取到房间[${roomId}]对战信息`);
            } else {
                const [user] = userStateData;
                log.info(`[${userId}]在房间[${roomId}]悔棋时，游离数据为：`, user);
                // 判断落子方是否为我方
                const lastBattleData = await BattleHistoryDao.getLastBattleMapHistory(user.battleId, userId);
                log.info(`[${userId}]在房间[${roomId}]悔棋时，当前落子方数据为：`, lastBattleData);
                if (lastBattleData.isRedMove !== user.first) {
                    socket.to(roomId).emit('backMoveResultRespApi', CryptorUtils.encrypt({
                        code: 'fail',
                        msg: '悔棋无效，落子方已交换'
                    }));
                    fn(CryptorUtils.encrypt({code: 'fail', msg: '悔棋无效，落子方已交换'}));
                    log.info(`[${userId}]悔棋时，房间[${roomId}]的落子方已交换`);
                } else {
                    // 数据库删除历史消息
                    await BattleHistoryDao.deleteLastMapHistory(user.battleId);
                    jobInstance.resetBattleStepTime(user.battleId, userId);

                    // 更新当前的落子方
                    await BattleDao.updateBattleCurrIsRedMove(user.battleId, !lastBattleData.isRedMove);
                    jobInstance.updateBattleData(user.battleId, {isRedMove: !lastBattleData.isRedMove});
                    log.info(`[${userId}]发生悔棋，已更新落子方[${!lastBattleData.isRedMove}]`);

                    const io = insUtils.getIns(INS_KEY.IO_KEY);
                    // 倒计时数据传递
                    io.to(roomId).emit('userTimeRespApi', CryptorUtils.encrypt({
                        code: 'success',
                        data: {
                            userList: jobInstance.getBattleTime(user.battleId),
                        },
                    }));

                    // 处理悔棋意愿
                    socket.to(roomId).emit('backMoveResultRespApi', CryptorUtils.encrypt({
                        code: 'success',
                        msg: '对方处理了悔棋意愿',
                        data: {result: result, userId: userId},
                    }));
                    fn(CryptorUtils.encrypt({code: 'success', msg: '允许悔棋'}));
                    log.info(`[${userId}]在[${roomId}]号房间处理悔棋意愿：[${result}]`);
                }
            }
        }
    }

    /**
     * 移动棋子逻辑
     * @param socket
     * @param battleId
     * @param userId
     * @param roomId
     * @param to
     * @param chessBox
     * @param fn
     */
    @Transaction
    async moveChessImpl(socket: any, battleId: string, userId: string, roomId: string, to: any, chessBox: object, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'moveChessImpl', {userId, roomId, battleId});

        // 检查落子方是否正确
        if (!(await this.checkMoveUser(userId))) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '落子方错误'}));
            log.info(`[${userId}]执行落子时落子方错误`);
        } else {
            // 查询对战表的数据
            const where = [];
            where.push(['room_id', roomId]);
            where.push(['user_status', USER_STATUS.BATTLE]);
            const userState: any = await UserStateDao.queryUserStateTable(where);
            if (userState.length === 0 || userState.length !== 2) {
                fn(CryptorUtils.encrypt({
                    code: 'fail',
                    msg: '无法获取对战数据',
                }));
                log.info(`无法获取到用户游离数据`, userState);
                fn(CryptorUtils.encrypt({code: 'fail', msg: '对战数据丢失'}));
            } else {
                const lastBattleData = await BattleHistoryDao.getLastBattleMapHistory(battleId, userId);
                const tryMoveGameMap = JSON.parse(boardUtils.tryMoveChess(JSON.parse(lastBattleData.gameMap), to));
                // 保存对战要移动棋子的数据
                const user = userState.find((user: any) => user.userId === userId);
                const enemy = userState.find((user: any) => user.userId !== userId);
                await battleUtils.saveMoveChessData(user, enemy, to, chessBox);

                // 检查游戏是否结束
                const isOver = await checkWin.judgeGameWin(userId, tryMoveGameMap, to);
                return {isOver, type: GAME_OVER_TYPE.BATTLE, nextIsRedMove: !lastBattleData.isRedMove};
            }
        }
    }

    /**
     * 同步房间数据
     * @param socket
     * @param userId
     * @param roomId
     * @param fn
     */
    syncRoomDataImpl = async (socket: any, userId: string, roomId: string, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'syncRoomDataImpl', {userId, roomId});

        // 检查用户是否在房间中
        const userStateRows: any = await UserStateDao.queryUserStateTable([['user_id', userId], ['room_id', roomId]]);
        if (userStateRows.length > 0) {
            await this.handleRoomData(socket, userId, roomId);
            fn(CryptorUtils.encrypt({code: 'success', msg: '同步房间数据成功'}));
            log.info(`[${userId}]对应的[${roomId}]号房间数据同步成功`);
        } else {
            // fn(CryptorUtils.encrypt({code: 'fail', msg: '房间已被解散'}));
            // log.info(`[${userId}]对应的[${roomId}]号房间数据同步失败(离开或被踢了)`);
        }
    }

    /**
     * 同步对战数据
     * @param socket
     * @param battleId
     * @param userId
     * @param roomId
     * @param fn
     */
    syncBattleDataImpl = async (socket: any, battleId: string, userId: string, roomId: string, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'syncBattleDataImpl', {
            userId,
            roomId,
            battleId
        });

        log.info(`[${userId}]请求同步房间[${roomId}]的对战数据`);
        const userStateData: any = await UserStateDao.queryUserStateTable([['user_id', userId], ['battle_id', battleId]]);
        if (userStateData.length !== 0) {
            const [userState] = userStateData;
            log.info(`[${userId}]对应房间[${roomId}]的对战数据`, userState);
            if (!userState.battleId) {
                fn(CryptorUtils.encrypt({code: 'success', msg: '对战已经结束'}));
                log.info(`[${userId}]对应房间[${roomId}]的数据异常(battleId为空)`);
            } else {
                // 获取房间的数据
                const battleData = await battleUtils.getBattleData(userId, userState.battleId, insUtils.getIns(INS_KEY.JOB_KEY));
                // log.info(`[${userId}]对战区[${userState.battleId}]的数据为：`, battleData);
                if (!battleData) {
                    fn(CryptorUtils.encrypt({code: 'success', msg: '对战已经结束'}));
                    log.info(`[${userId}]的对战数据在同步过程中对局已经结束`);
                    return;
                }
                // userId是对战用户
                if (battleData.some((user: any) => user.userId === userId)) {
                    // 获取用户数据
                    const user = battleData.find((user: any) => user.userId === userId);
                    const enemy = battleData.find((user: any) => user.userId !== userId);
                    socket.emit('syncBattleDataRespApi', CryptorUtils.encrypt({
                        code: 'success',
                        msg: '获取房间对战数据成功',
                        data: {
                            roomUser: user,
                            roomEnemy: enemy,
                        },
                    }));
                    fn(CryptorUtils.encrypt({code: 'success', msg: '获取房间对战数据成功'}));
                    log.info(`[${userId}]已同步房间的对战数据`);
                } else {
                    // 观战用户获取数据
                    const user = battleData.find((user: any) => user.first);
                    const enemy = battleData.find((user: any) => !user.first);
                    socket.emit('syncBattleDataRespApi', CryptorUtils.encrypt({
                        code: 'success',
                        msg: '获取房间观战数据成功',
                        data: {
                            roomUser: user,
                            roomEnemy: enemy,
                        },
                    }));
                    fn(CryptorUtils.encrypt({code: 'success', msg: '获取房间观战数据成功'}));
                    log.info(`[${userId}]已同步房间的观战数据`);
                }
            }
        } else {
            fn(CryptorUtils.encrypt({code: 'success', msg: '对战已经结束'}));
            log.info(`[${userId}]同步房间的对战数据失败(用户不在房间中)`);
        }
    }

    /**
     * 发起认输
     * @param socket
     * @param userId
     * @param fn
     */
    @Transaction
    @CatchException
    async userAdmitDefeatImpl(socket: any, userId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'userAdmitDefeatImpl', {userId});

        log.info(`[${userId}]发起认输`);
        const userState = await UserStateDao.getUserStateByUserId(userId);
        if (!userState) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '房间数据错误，请重试'}));
            log.info(`用户[${userId}]无对战房间信息`);
        } else if (userState.battleId) {
            fn(CryptorUtils.encrypt({code: 'success', msg: '发起认输操作成功'}));
            await roomUtils.handleGameResult(userState.battleId, userId, GAME_OVER_TYPE.ADMIT_DEFEAT);
        }
        log.info(`[${userId}]已认输`);
    }

    /**
     * 玩家聊天
     * @param socket
     * @param userId
     * @param roomId
     * @param content
     * @param fn
     */
    @Transaction
    @CatchException
    async userChatImpl(socket: any, userId: string, roomId: string, content: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'userChatImpl', {userId, roomId});

        // 查询对战信息
        const where = [];
        where.push(['user_id', userId]);
        where.push(['room_id', roomId]);
        const userStateData: any = await UserStateDao.queryUserStateTable(where);
        if (userStateData.length === 0) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '消息发送失败'}));
            log.info(`[${userId}]发送消息[${content}]失败，因用户无游离数据`);
        } else {
            const [user] = userStateData;
            const userDetail = await UserDao.getUserByPrimaryKey(userId);
            if (!userDetail) {
                fn(CryptorUtils.encrypt({code: 'fail', msg: '消息发送失败'}));
                log.info(`[${userId}]发送消息[${content}]失败，无法获取用户信息`);
            } else {
                // 数据库记录聊天消息
                await BattleChatDao.createChatInfo(userId, user.battleId, content);
                // 消息转发给对方
                socket.to(roomId).emit('chatRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: '消息传递',
                    data: {
                        userId: userId,
                        userName: userDetail.userName,
                        content: content,
                    },
                }));
            }
        }
    }

    /**
     * 游戏胜利
     * @param socket
     * @param userId
     * @param type
     */
    @Transaction
    async gameWinImpl(socket: any, userId: string, type: string) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'gameWinImpl', {userId});

        const userState = await UserStateDao.getUserStateByUserId(userId);
        if (!userState) {
            log.info(`用户[${userId}]无对战房间信息`);
        } else {
            await roomUtils.handleGameResult(userState.battleId, userId, type);
        }
    }

    /**
     * 断开连接逻辑
     * @param socket
     * @param reason
     * @return {Promise<void>}
     */
    @Transaction
    @CatchException
    async disconnectImpl(socket: any, reason: string) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'disconnectImpl');

        const token = socketUtils.getToken(socket);
        log.info(`检测到token:[${token}]已离线，离线原因为：${reason}`);

        const allSockets = socketUtils.getSocketsBy(token);
        // 查询游离数据
        const userStateRows: any = await UserStateDao.queryUserStateTable([['token', token]]);
        log.info(`用户游离数据为:`, userStateRows);

        if (userStateRows.length > 0 && allSockets.length === 0) {
            const [userState] = userStateRows;
            const {userId, userStatus, battleId, roomId} = userState;
            const offlineTime = new Date();
            // 为用户状态游离表更新断开时间
            log.info(`开始更新用户[${userId}]的断开时间, offlineTime:[${dateUtils.formatDate(offlineTime)}]`);
            await UserStateDao.updateUserStateTable([['disconnect_time', offlineTime]], [['user_id', userId]]);
            // 继续判断是否在房间中
            if (roomId != null) {
                // 观战用户不需要通知
                if (userStatus !== USER_STATUS.WATCH) {
                    // 若该用户发生了对战，则为用户更新当前局的对战离线时间
                    battleId && await BattleUserDao.updateBattleUserOfflineTime(battleId, userId, roomId, offlineTime);

                    log.info(`开始告诉对手方我方已离线的信息，userId:[${userId}]`);
                    // 通知房间内的对手，该用户已离线，并进行倒计时自动判定负
                    socket.to(roomId).emit('enemyOfflineRespApi', CryptorUtils.encrypt({
                        code: 'success',
                        msg: '对手暂时离线',
                        data: {
                            userId: userId,
                            offlineTime: offlineTime.getTime(),
                            offlineTimeout: BATTLE.OFFLINE_TIME_SECONDS,
                        },
                    }));
                } else {
                    log.info(`[${userId}]为观战用户，不需要对房间内的用户发送离线通知`);
                }
                log.info(`[${userId}]暂时断开，服务器将会话保持${BATTLE.OFFLINE_TIME_SECONDS / 60}分钟`);
            } else {
                log.info(`[${userId}]无房间信息，仅更新游戏表数据中的超时时间:${dateUtils.formatDate(offlineTime)}`);
            }
        } else {
            log.info(`token:[${token}]已断开连接（未登录/正常退出）`);
        }
    }

    /**
     * 检查session是否过期
     * @param userId
     * @param token
     * @param ip
     */
    checkSessionExpired = async (userId: string, token: string, ip: string) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'watchListImpl', {userId});
        const userState = await UserStateDao.getUserStateByUserId(userId);
        // log.debug("会话预检，用户信息为：", userState);
        return userState ? userState.token !== token : true;
    }

    /**
     * 检查棋子落子方是否正确
     * @param userId
     * @returns true: 正确 false: 不正确
     */
    checkMoveUser = async (userId: string) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'checkMoveUser', {userId});

        const userState = await UserStateDao.getUserStateByUserId(userId);
        if (!userState) {
            log.info(`[${userId}]无游离状态数据`);
            return false;
        }
        return await BattleUserDao.checkMoveUser(userId, userState.battleId);
    }

    /**
     * 恢复会话数据（仅处理登录时是否需要恢复）
     * @param socket
     * @param userId
     */
    async loginAfterSessionRecover(socket: any, userId: string) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'loginAfterSessionRecover', {userId});

        // 查询用户状态表中是否有数据
        const userState = await UserStateDao.getUserStateByUserId(userId);
        log.info(`[${userId}]查询用户状态表中的数据，结果:`, userState);
        if (!userState) {
            log.warn(`[${userId}]无游离数据，会话恢复终止`);
            return false;
        }
        const {userPage, roomId, battleId} = userState;
        // 检查是否还有正在进行的对局
        const isInBattle = await BattleDao.checkUserInBattle(userId);
        // 按用户所处页面进行数据恢复
        if (isInBattle || userPage === PAGE_STATUS.BOARD) {
            // 如果在匹配平台，就一定有房间号
            socket.join(roomId);

            // 如果在对局，就一定有battleId
            log.info(`[${userId}]有对战区[${battleId}]，开始恢复数据（对局）`);
            socket.emit('sessionRecoverNotifyApi', CryptorUtils.encrypt({
                code: 'success',
                msg: `进入对局，请稍候...`,
            }));
            log.info(`[${userId}]所在对战区[${battleId}]对局未结束，开始恢复数据`);
            // 清除对战用户表中的离线时间
            await BattleUserDao.updateBattleUserOfflineTime(battleId, userId, roomId, null);
            // 获取房间的数据
            const battleData = await battleUtils.getBattleData(userId, battleId, insUtils.getIns(INS_KEY.JOB_KEY));
            // log.info(`[${userId}]所在对战区[${battleId}]房间对战数据为`, battleData);
            if (!battleData) {
                socket.emit('sessionRecoverRespApi', CryptorUtils.encrypt({
                    code: 'S000001',
                    msg: '对局已经结束',
                }));
                socket.leave(roomId);
                log.info(`[${userId}]所在对战区[${battleId}]在恢复过程中对局已经结束`);
                return;
            }

            // 获取用户数据
            const user: any = battleData.find((user: any) => user.userId === userId);
            const enemy: any = battleData.find((user: any) => user.userId !== userId);
            socket.emit('sessionRecoverRespApi', CryptorUtils.encrypt({
                code: 'S000002',
                msg: '恢复对局数据',
                data: {
                    roomId: roomId,
                    roomUser: user,
                    roomEnemy: enemy,
                    role: USER_ROLE.BATTLE,
                    battleId: battleId,
                    page: userPage,
                    joinType: userState.joinType
                },
            }));
            // 通知对手已经上线
            socket.to(roomId).emit('enemyOnlineRespApi', CryptorUtils.encrypt({
                code: 'success',
                msg: '对手已经上线',
            }));
            // 查询敌人有没有离线
            if (enemy.offlineTime) {
                // 通知房间内的对手，该用户已离线，并进行倒计时自动判定负
                socket.emit('enemyOfflineRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: '对手暂时离线',
                    data: {
                        userId: userId,
                        offlineTime: dateUtils.strToDate(enemy.offlineTime).getTime(),
                        offlineTimeout: BATTLE.OFFLINE_TIME_SECONDS,
                    },
                }));
                log.info(`[${userId}]房间中的对手[${enemy.userId}]暂时断开`);
            }

            log.info(`[${userId}]所在对战区[${battleId}]已同步对战数据`);
        } else if (userPage === PAGE_STATUS.WATCH) {
            log.info(`[${userId}]有对战区[${battleId}]，开始恢复数据（观战）`);
            // 如果在匹配平台，就一定有房间号
            socket.join(roomId);
            // 检查该房间是否有对局数据
            const battleData: any = await BattleDao.queryBattleTable([['room_id', roomId], ['room_status', ROOM_STATUS.BATTLE]]);
            if (battleData.length > 0) {
                const [battle] = battleData;

                socket.emit('sessionRecoverNotifyApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: `进入对局，请稍候...`,
                    data: {
                        battleId: battle.id,
                        // 是否为新开对局
                        isNewBattle: battle.id !== battleId,
                    }
                }));

                // 更新对局信息
                const columns = [];
                columns.push(['battle_id', battle.id]);
                columns.push(['user_status', USER_STATUS.WATCH]);
                columns.push(['user_page', PAGE_STATUS.WATCH]);
                columns.push(['room_id', battle.roomId]);
                await UserStateDao.updateUserStateTable(columns, [['user_id', userId]]);

                // 观战用户恢复数据
                socket.emit('sessionRecoverRespApi', CryptorUtils.encrypt({
                    code: 'S000002',
                    msg: '恢复观战数据',
                    data: {
                        role: USER_ROLE.WATCH,
                        roomId: roomId,
                    },
                }));
                log.info(`已通知[${userId}]同步观战数据`);
            } else {
                // 对局已经结束
                socket.emit('sessionRecoverRespApi', CryptorUtils.encrypt({
                    code: 'S000004',
                    msg: '对局已经结束',
                }));
                log.info(`[${userId}]观战的对战区[${battleId}]对局已结束(观战)`);
            }
        } else if (userPage === PAGE_STATUS.PLAYER_RANDOM || userPage === PAGE_STATUS.PLAYER_FREEDOM) {
            // 如果在匹配平台，就一定有房间号
            socket.join(roomId);
            // 通知对手，我方已加入(通知此事件客户端会主动查寻房间数据)
            socket.to(roomId).emit('enemyJoinRoomRespApi', CryptorUtils.encrypt({
                code: 'success',
                msg: '对手加入房间'
            }));
            log.info(`[${userId}]无对战数据，尝试恢复房间[${roomId}]数据`);
            // 引导客户端到指定页面
            socket.emit('sessionRecoverRespApi', CryptorUtils.encrypt({
                code: 'S000002',
                msg: null,
                data: {
                    page: userPage,
                    roomId: roomId,
                    joinType: userState.joinType
                },
            }));
            log.info(`[${userId}]有房间[${roomId}]的数据，当前页:${userPage}`);

            log.info(`主动为用户[${userId}]同步房间[${roomId}]的数据`);
            await this.handleRoomData(socket, userId, roomId);
        } else {
            socket.emit('sessionRecoverRespApi', CryptorUtils.encrypt({
                code: 'S000002',
                msg: null,
                data: {
                    page: userPage,
                },
            }));
            log.info(`[${userId}]停留到默认页面，默认页:${userPage}`);
        }
    };

    /**
     * 数据恢复
     * @param socket
     * @param userId
     * @param roomId
     * @param battleId
     */
    @Transaction
    @CatchException
    async recoverDetail(socket: any, userId: string, roomId: string, battleId: string) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'recoverDetail', {userId, roomId, battleId});

        const token = socketUtils.getToken(socket);
        // 查询游离数据
        const userState = await UserStateDao.getUserStateByUserId(userId);
        log.info(`token:[${token}]关联的用户游离数据为:`, userState);
        if (userState) {
            // 若存在历史会话，且会话Id不一致，则账号已经在别处登录
            const allSockets = socketUtils.getSocketsBy(userState.token);
            if (allSockets.length > 0 && token !== userState.token) {
                // 发送账号冲突请求
                socket.emit('userConflictRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: '账号已在别处登录',
                }));
                // 将socket关闭
                socket.disconnect();
                // 会话已过期，因为已在别处登录了
                log.info(`token:[${token}]已在其它地方登录`);
            } else {
                // 清除断线时间
                await UserStateDao.updateUserStateTable([['disconnect_time', null]], [['user_id', userId]]);
                log.info(`token:[${token}]关联的用户[${userId}]已清除断线时间`);

                // 恢复会话
                await this.loginAfterSessionRecover(socket, userId);
            }
        } else {
            // 带userId恢复数据，但又无游离数据，表示会话已经过期了
            socket.emit('sessionRecoverRespApi', CryptorUtils.encrypt({
                code: 'S000003',
                msg: '您的会话已经过期，请重新登录',
            }));
            // 会话已过期
            log.info(`token:[${token}]关联的用户[${userId}]会话信息已过期`);
        }
    }

    /**
     * 获取复盘详情
     * @param socket
     * @param userId
     * @param battleId
     * @param pageSize
     * @param fn
     */
    battleReviewDetailImpl = async (socket: any, userId: string, battleId: string, pageSize: number, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'battleReviewDetailImpl', {userId, battleId});

        // 获取对局详情(分页)
        const pageBattleData = await BattleHistoryDao.getBattleReviewDetail(userId, battleId, pageSize);
        log.info(`[${userId}]请求了[${battleId}]的详情数据`);
        fn(CryptorUtils.encrypt({
            code: 'success',
            msg: '获取要复盘的对局详情',
            data: pageBattleData,
        }));
        log.info(`[${userId}]所关联的对局[${battleId}]复盘详情数据获取完成`);
    }

    /**
     * 获取分享的链接
     * @param socket
     * @param userId
     * @param battleId
     * @param fn
     */
    getShareLinkImpl = async (socket: any, userId: string, battleId: string, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'getShareLinkImpl', {userId, battleId});

        const userDetail = await UserDao.getUserByPrimaryKey(userId);
        if (!userDetail) {
            // 返回分享链接和内容
            fn(CryptorUtils.encrypt({
                code: 'fail',
                msg: '用户信息不存在',
            }));
            return;
        }
        // 查询用户在该对局下是否已经有分享的码
        const shareRows: any = await ShareDao.queryShareTable([['user_id', userId], ['battle_id', battleId]]);
        let shareCode;
        if (shareRows.length > 0) {
            // 有分享记录
            shareCode = shareRows[0].shareCode;
        } else {
            // 创建分享码
            shareCode = constUtils.getRandomId(8);
            // 在分享表创建一条数据
            await ShareDao.saveShare(battleId, userId, userDetail.userName, shareCode, null, null);
            log.info(`[${userId}]所在的对战区[${battleId}]已创建分享记录，码：${shareCode}`);
        }
        log.info(`[${userId}]所在的对战区[${battleId}]分享码为：${shareCode}`);

        const url = `${APP.SHARE_DOMAIN}/shared?code=${shareCode}`;
        const shareLinkText = `[${userDetail.userName}]分享了一盘对局给你~\n${url}`;

        // 返回分享链接和内容
        fn(CryptorUtils.encrypt({
            code: 'success',
            msg: '分享链接获取成功',
            data: shareLinkText,
        }));
        log.info(`[${userId}]所在的对战区[${battleId}]分享数据获取成功`);
    }

    /**
     * 对局复盘
     * @param socket
     * @param userId
     * @param pageNum
     * @param pageSize
     * @param fn
     */
    battleReviewImpl = async (socket: any, userId: string, pageNum: number, pageSize: number, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'battleReviewImpl', {userId});

        // 获取可复盘的对局
        const pageList: any = await BattleUserDao.getBattleReviewListByPage(userId, pageNum, pageSize);
        log.info(`[${userId}]请求对局复盘数据完成`);
        const {dataTotal, list} = pageList;
        fn(CryptorUtils.encrypt({
            code: 'success',
            msg: '获取复盘列表',
            data: {
                list: list,
                pageSize: pageSize,
                pageNum: pageNum,
                dataTotal: dataTotal,
            },
        }));
        // 切换到复盘页面
        await UserStateDao.updateUserStateTable([['user_page', PAGE_STATUS.REVIEW]], [['user_id', userId]]);
        log.info(`[${userId}]请求了对局复盘数据列表，共[${list.length}]条数据`);
    }

    /**
     * 离开复盘列表
     * @param socket
     * @param userId
     * @param fn
     */
    leaveReviewListImpl = async (socket: any, userId: string, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'leaveReviewListImpl', {userId});

        log.info(`[${userId}]请求离开复盘列表`);
        await UserStateDao.updateUserStateTable([['user_page', PAGE_STATUS.PLATFORM]], [['user_id', userId]]);
        fn(CryptorUtils.encrypt({code: 'success', msg: '已离开复盘列表'}));
        log.info(`[${userId}]已离开复盘列表`);
    }


    /**
     * 处理同步房间数据逻辑
     * @param socket
     * @param userId
     * @param roomId
     */
    handleRoomData = async (socket: any, userId: string, roomId: string) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'handleRoomData', {userId, roomId});

        log.info(`[${userId}]请求同步房间[${roomId}]数据`);
        // 获取房间的数据
        const roomDataList: any = await UserStateDao.getRoomData(roomId);
        const userInRoom = roomDataList.some((user: any) => user.userId === userId);
        // 用户在房间时，以当前用户为起始方，否则按谁先手为起始方
        const user = roomDataList.find((user: any) => userInRoom ? user.userId === userId : user.first);
        const enemy = roomDataList.find((user: any) => userInRoom ? user.userId !== userId : !user.first);
        socket.emit('syncRoomDataRespApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '获取房间数据成功',
            data: {
                roomUser: user,
                roomEnemy: enemy,
            },
        }));
        log.info(`[${userId}]已同步房间数据`);
    }


    /**
     * 双方都准备后，保存棋盘信息
     * @param roomId
     * @param user
     * @param enemy
     */
    async saveBoardData(roomId: string, user: any, enemy: any) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'saveBoardData', {userId: user.userId, roomId});

        const io = insUtils.getIns(INS_KEY.IO_KEY);

        log.info(`开始校验房间:[${roomId}]是否为重复创建对局`);
        // 检测是否已创建过对战
        const rows: any = await BattleDao.countCreatedBattleByRoomId(roomId);
        log.info(`房间:[${roomId}]已创建的对局数据为:`, rows);
        if (rows.length > 0) {
            log.info(`房间:[${roomId}]已存在对战数据，已停止重复创建对局`);
            return {
                result: false,
                battleId: null,
            }
        }

        /** === 对手已准备，更新双方信息为匹配成功 === */
        await UserStateDao.updateUserStateTable([['room_status', ROOM_STATUS.MATCH_SUCCESS]], [['user_id', user.userId]]);
        await UserStateDao.updateUserStateTable([['room_status', ROOM_STATUS.MATCH_SUCCESS]], [['user_id', enemy.userId]]);
        // 通知房间内的所有人匹配成功
        io.to(roomId).emit('matchSuccessRespApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '匹配成功',
        }));

        /** === 创建对战数据 === */
        const battleId = constUtils.getRandomId(32, 10);
        await BattleDao.createBattleData(battleId, roomId, ROOM_STATUS.BATTLE);

        /** === 创建对战用户数据 === */
        // 自己的数据
        await BattleUserDao.createBattleUserData(battleId, user.userId, user.userName, enemy.userId, enemy.userName, roomId, user.first);
        // 对方的数据
        await BattleUserDao.createBattleUserData(battleId, enemy.userId, enemy.userName, user.userId, user.userName, roomId, enemy.first);

        /** === 创建对战历史步骤数据 === */
            // 自己的数据
        const defaultBoxChess = JSON.stringify({show: false});
        await BattleHistoryDao.createBattleMapHistory(battleId, user.userId, JSON.stringify(user.gameMap), user.isRedMove, user.stepCount,
            null, JSON.stringify({}), defaultBoxChess, user.allTime, user.stepTime);
        // 对方的数据
        await BattleHistoryDao.createBattleMapHistory(battleId, enemy.userId, JSON.stringify(enemy.gameMap), enemy.isRedMove, enemy.stepCount,
            null, JSON.stringify({}), defaultBoxChess, enemy.allTime, enemy.stepTime);

        /** === 更新游戏游离表中的数据 === */
        const updUserColumns = [];
        updUserColumns.push(['battle_id', battleId]);
        updUserColumns.push(['user_status', USER_STATUS.BATTLE]);
        updUserColumns.push(['user_page', PAGE_STATUS.BOARD]);
        updUserColumns.push(['room_status', ROOM_STATUS.BATTLE]);
        await UserStateDao.updateUserStateTable(updUserColumns, [['user_id', user.userId]]);

        const updEnemyColumns = [];
        updEnemyColumns.push(['battle_id', battleId]);
        updEnemyColumns.push(['user_status', USER_STATUS.BATTLE]);
        updEnemyColumns.push(['user_page', PAGE_STATUS.BOARD]);
        updEnemyColumns.push(['room_status', ROOM_STATUS.BATTLE]);
        await UserStateDao.updateUserStateTable(updEnemyColumns, [['user_id', enemy.userId]]);

        /** === 创建该对战房间的倒计时数据 === */
        await insUtils.getIns(INS_KEY.JOB_KEY).createBattleUserTime(battleId);

        // 通知可以进入对战
        io.sockets.in(roomId).emit('allowInBattleApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '允许进入对局',
            data: {
                // 将双方信息返回给客户端（这里不区分敌我方，因为有可能是对手方准备触发的数据）
                playOne: user,
                playTwo: enemy,
                battleId: battleId
            },
        }));

        log.info(`房间:[${roomId}]对局已完成创建`);
        return {
            result: true,
            battleId: battleId,
        }
    };

    /**
     * 在线数据统计
     * @param socket
     * @param userId
     * @param fn
     */
    onlineCountImpl = async (socket: any, userId: string, fn: Function) => {
        socket.emit('onlineCountRespApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '在线数据累计',
            data: {
                action: 'update',
                onlineCount: await UserStateDao.queryOnlineCount(),
                battleCount: await BattleDao.queryBattleCount(),
            },
        }));
    }

    /**
     * 房间中人观战人数信息统计
     * @param socket
     * @param userId
     * @param roomId
     * @param fn
     */
    watchCountImpl = async (socket: any, userId: string, roomId: string, fn: Function) => {
        const user: any = UserDao.getUserByPrimaryKey(userId);
        socket.emit('watchNotifyRespApi', CryptorUtils.encrypt({
            code: 'success',
            msg: '观战人数信息累计',
            data: {
                action: 'update',
                // action: add 和 sub 时 watchUserId 必需有值
                watchUserId: userId,
                watchUserName: user.userName,
                // action: update 时 watchUserList 为全量数据
                watchUserList: await UserStateDao.queryWatchList(userId, roomId),
            },
        }));
    }

    /**
     * 桌子列表查询
     * @param socket
     * @param userId
     * @param bindRoomLocation 是否按当前用户所在的房间位置进行分页查询
     * @param pageNum
     * @param pageSize
     * @param fn
     */
    roomListImpl = async (socket: any, userId: string, bindRoomLocation: boolean, pageNum: number, pageSize: number, fn: Function) => {
        const log = global.logUtils.createContext('SocketServiceImpl', 'roomListImpl', {userId});

        log.info(`开始为用户[${userId}]查询桌子列表`);
        // 绑定用户所在的房间时，按房间号重新分页
        let roomId = null;
        if (bindRoomLocation) {
            // 查询用户游离数据，并获取房间号
            const userState = await UserStateDao.getUserStateByUserId(userId);
            log.info(`用户[${userId}]的游离数据为：`, userState);
            if (userState) {
                roomId = userState.roomId;
                if (roomId) {
                    // 当用户存在某个房间号时，根据用户所在的房间号查询分页数据
                    pageNum = Math.ceil(roomId / pageSize);
                }
            }
        }
        // 查询桌子数据
        const pageList: any = await UserStateDao.getRoomListByPage(userId, pageNum, pageSize);
        log.info(`[${userId}]请求桌子数据完成`);
        const {dataTotal, list} = pageList;
        fn(CryptorUtils.encrypt({
            code: 'success',
            msg: '获取桌子列表',
            data: {
                list: list,
                pageSize: pageSize,
                pageNum: pageNum,
                dataTotal: dataTotal,
                roomId: roomId,
            },
        }));
        log.info(`[${userId}]请求桌子列表，共[${list.length}]条数据`);
    }


    /**
     * 上传图片
     * @param socket
     * @param userId
     * @param fileName
     * @param fileSize
     * @param base64
     * @param contentType
     * @param fn
     */
    @Transaction
    @CatchException
    async uploadBase64Impl(socket: any, userId: string, fileName: string, fileSize: number, base64: string, contentType: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'uploadBase64Impl', {userId});
        const user: any = await UserDao.getUserByPrimaryKey(userId);
        if (!user) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '查询不到用户信息'}));
        } else if (USER_TYPE.TOURIST_USER === user.userType) {
            fn(CryptorUtils.encrypt({code: 'fail', msg: '游客无法上传图片'}));
        } else {
            log.info(`开始上传图片：${fileName}`);
            fileUtils.writeBase64(userId, fileName, base64, fileSize, contentType)
                .then(async (fileId: any) => {
                    log.info(`文件[${fileName}]上传成功`);
                    fn(CryptorUtils.encrypt({
                        code: 'success',
                        msg: '文件上传成功',
                        data: {
                            fileId: fileId,
                        }
                    }));
                })
                .catch(err => {
                    log.info(`文件[${fileName}]上传失败，原因：`, err);
                    fn(CryptorUtils.encrypt({code: 'fail', msg: '文件上传失败'}));
                })
        }
    }

    @Transaction
    @CatchException
    async kickUserImpl(socket: any, userId: string, kickUserId: string, roomId: string, fn: Function) {
        const log = global.logUtils.createContext('SocketServiceImpl', 'kickUserImpl', {userId, roomId});
        const userStateRows: any = await UserStateDao.queryUserStateTable([['user_id', kickUserId], ['room_id', roomId]]);
        if (!userStateRows || userStateRows.length === 0) {
            log.info(`无法查询到用户游离数据，用户账号[${kickUserId}],房间[${roomId}],游离数据为:`, userStateRows)
            fn(CryptorUtils.encrypt({code: 'fail', msg: '用户数据错误'}));
        } else {
            const [userState] = userStateRows;
            const {token} = userState;
            log.info(`开始踢出房间[${roomId}]中的[${kickUserId}]`);
            await roomUtils.leaveRoom(kickUserId, roomId, false);

            log.info(`开始发送房间[${roomId}]，用户[${kickUserId}]的踢出提醒信息, token: ${token}`);
            // 提示此用户被踢出房间
            const socketList = socketUtils.getSocketsBy(token);
            // log.info(`被踢用户的房间[${roomId}]，socket数量：${socketList.length}`);
            for (let kickSocket of socketList) {
                // 通知对方被踢了
                kickSocket.emit('kickUserRespApi', CryptorUtils.encrypt({
                    code: 'success',
                    msg: '您被踢出了房间',
                    data: {
                        userId: kickUserId,
                        roomId: roomId
                    }
                }));
                // 之后离开房间
                kickSocket.leave(roomId);
            }
            // 记录踢人流水
            await RoomFlowDao.saveRoomFlow(userId, roomId, kickUserId, ROOM_FLOW_TYPE.KICK, null);
            // 通知我方操作已完成
            fn(CryptorUtils.encrypt({code: 'success', msg: '踢出用户操作成功'}));
        }
    }
}

export default SocketServiceImpl;